package xj.graph2d;

import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.FocusEvent;
import java.awt.event.FocusListener;
import java.awt.event.InputEvent;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.awt.event.MouseWheelEvent;
import java.awt.event.MouseWheelListener;
import java.awt.geom.AffineTransform;
import java.awt.geom.NoninvertibleTransformException;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

import javax.swing.JPanel;
import javax.swing.KeyStroke;

import xj.app.DrawTool;
import xj.util.gui.Point2DFloat;
import xj.util.gui.Rectangle2DFloat;

/* 
 *  The dimension and position in DrawCanvas are in float
 */

public class DrawCanvas extends JPanel implements FocusListener {

  public DrawCanvas(int width, int height, View view) {
    this(width, height, view, false, null);
  }

  public DrawCanvas(int width, int height, View view, 
		    boolean thumbnail,
		    DrawCanvas master) {
    super(true);
    preferedCanvasSize = new Dimension(width, height);
    this.view = view;
    this.thumbnail = thumbnail;
    this.master = master;
    if (thumbnail) {
      setOpaque(false);
      setBackground(new Color(0, 0, 0, 0));
    } else {
      addFocusListener(this);
      addListener(listener = new DrawCanvasEventListener(this));
      // repaintCollector = new CoalescingRepaintCollector();
    }
    setVirtualCanvasRectangle(new Rectangle2DFloat(0, 0, width, height));
    //restoreDefaultVirtualCanvasRectangle();
  }

  public View getView() {
    return view;
  }

  @Override
  public Dimension getPreferredSize() {
    return preferedCanvasSize;
  }

  public void restoreDefaultVirtualCanvasRectangle() {
    if (view != null) {
      Rectangle2D r = view.getDefaultVirtualCanvasRectangle();
      if (r != null) {
	setVirtualCanvasRectangle(r);
      }
    }
  }

  public void setVirtualCanvasRectangle(Rectangle2D r) {
    if (virtualCanvasRectangle == null) {
      virtualCanvasRectangle = new Rectangle.Float();
    }
    if (r != null) {
      virtualCanvasRectangle.setRect(r);
    }
    resetTransform();
    if (scrollcanvas != null) {
      scrollcanvas.adjust();
    }
  }

  public Rectangle2D getVirtualCanvasRectangle() {
    return virtualCanvasRectangle;
  }

  public Point virtualToPhysical(Point2D p) {
    if (p != null) {
      double[] pts = new double[4];
      pts[0] = pts[2] = p.getX();
      pts[1] = pts[3] = p.getY();
      if (transform != null) {
	transform.transform(pts, 0, pts, 2, 1);
      }
      return new Point((int) pts[2], (int) pts[3]);
    } else {
      return null;
    }
  }

  public Point2D physicalToVirtual(Point p) {
    if (p != null) {
      double[] pts = new double[4];
      pts[0] = pts[2] = p.getX();
      pts[1] = pts[3] = p.getY();
      if (transform != null) {
	try {
	  transform.inverseTransform(pts, 0, pts, 2, 1);
	} catch (NoninvertibleTransformException e) {
	}
      }
      return new Point2DFloat((float) pts[2], (float) pts[3]);
    } else {
      return null;
    }
  }

  public Rectangle virtualToPhysical(Rectangle2D r) {
    if (r != null) {
      double[] pts = new double[8];
      pts[0] = pts[4] = r.getX();
      pts[1] = pts[5] = r.getY();
      pts[2] = pts[6] = pts[0] + r.getWidth();
      pts[3] = pts[7] = pts[1] + r.getHeight();
      if (transform != null) {
	transform.transform(pts, 0, pts, 4, 2);
      }
      return new Rectangle((int) pts[4], (int) pts[5], (int) (pts[6] - pts[4]),
	  (int) (pts[7] - pts[5]));
    } else {
      return null;
    }
  }

  public Rectangle2D physicalToVirtual(Rectangle r) {
    if (r != null) {
      double[] pts = new double[8];
      pts[0] = pts[4] = r.getX();
      pts[1] = pts[5] = r.getY();
      pts[2] = pts[6] = pts[0] + r.getWidth();
      pts[3] = pts[7] = pts[1] + r.getHeight();
      if (transform != null) {
	try {
	  transform.inverseTransform(pts, 0, pts, 4, 2);
	} catch (NoninvertibleTransformException e) {
	}
      }
      return new Rectangle2D.Float((float) pts[4], (float) pts[5],
	  (float) (pts[6] - pts[4]), (float) (pts[7] - pts[5]));
    } else {
      return null;
    }
  }

  public Rectangle2D getViewportRectangle() {
    Dimension d = getSize(); // device space
    return physicalToVirtual(new Rectangle(0, 0, d.width, d.height));
  }

  public void setViewportLocation(Point2D p) {
    if (p != null) {
      setViewportLocation((float) p.getX(), (float) p.getY(), true);
    }
  }

  public void setViewportLocation(float x, float y) {
    setViewportLocation(x, y, true);
  }

  public void setViewportLocation(float x, float y, boolean notify) {
    if (debug) {
      System.out.println("DrawCanvas.setViewportLocation() x=" + x + " y=" + y);
    }

    boolean adjusted = false;
    if (xOrigin != x) {
      xOrigin = x;
      adjusted = true;
    }
    if (yOrigin != y) {
      yOrigin = y;
      adjusted = true;
    }
    if (adjusted) {
      if (notify && scrollcanvas != null) {
	scrollcanvas.setScrollPosition(x, y, false);
      }
      resetTransform();
      repaint();
    }
  }

  public Point2D getViewportLocation() {
    return new Point2DFloat(xOrigin, yOrigin);
  }

  public float getScaleFactor() {
    return scaleFactor;
  }

  public void setScaleFactor(float f) {
    if (f != scaleFactor) {
      scaleFactor = f;
      resetTransform();
      if (scrollcanvas != null) {
	scrollcanvas.adjust();
      }
      repaint();
    }
  }

  public boolean getEditingMode() {
    return editMode;
  }

  public void setEditingMode(boolean b) {
    editMode = b;
  }

  public Point2D getMouseLocation() {
    return physicalToVirtual(listener.getMouseLocation());
  }

  public Graphics2D getGraphics2D() {
    return (Graphics2D) getGraphics();
  }

  @Override
  public void paintComponent(Graphics g) {
    super.paintComponent(g);

    Graphics2D g2 = (Graphics2D) g;
    Rectangle r = g.getClipBounds();
    setRenderingHints(g2);

    if (debug) {
      System.out.println("DrawCanvas.paintComponent(): " + r);
    }

    if (view != null) {
      Color bg;
      if (isThumbNail()) {
	// bg = Color.gray;
	bg = new Color(128, 128, 128, 128);
	Rectangle r2 = null;
	if (master != null) {
	  r2 = virtualToPhysical(master.getViewportRectangle());
	}

	if (debug) {
	  System.out.println("DrawCanvas.paintComponent(): r2=" + r2);
	}

	if (r == null) {
	  Dimension dim = getSize();
	  g2.setColor(bg);
	  g2.fillRect(0, 0, dim.width, dim.height);
	  if (r2 != null) {
	    bg = view.getThumbNailBackgroundColor();
	    g2.setColor(bg);
	    if (drawThumbViewport) { 
	      g2.fillRect(r2.x, r2.y, r2.width, r2.height);
	    } else { 
	      g2.fillRect(0, 0, dim.width, dim.height);
	    }
	  }
	  setTransform(g2);
	  view.paintCanvas(g2, false, true);
	} else {
	  g2.setColor(bg);
	  g2.fillRect(r.x, r.y, r.width, r.height);
	  if (r2 != null) {
	    bg = view.getThumbNailBackgroundColor();
	    g2.setColor(bg);
	    if (drawThumbViewport) { 
	      g2.fillRect(r2.x, r2.y, r2.width, r2.height);
	    } else { 
	      g2.fillRect(r.x, r.y, r.width, r.height);
	    }
	  }
	  setTransform(g2);
	  view.paintCanvas(g2, physicalToVirtual(r), false, true);
	}
      } else {
	if (r == null) {
	  Dimension dim = getSize();
	  g2.setColor(Color.darkGray);
	  g2.fillRect(0, 0, dim.width, dim.height);
	  setTransform(g2);
	  //setRenderingHints(g2);
	  view.paintCanvas(g2);
	} else {
	  g2.setColor(Color.darkGray);
	  g2.fillRect(r.x, r.y, r.width, r.height);
	  setTransform(g2);
	  //setRenderingHints(g2);
	  view.paintCanvas(g2, physicalToVirtual(r));
	}
	Iterator<RShape> iter = repaintShapes.iterator();
	while (iter.hasNext()) {
	  RShape rs = iter.next();
	  if (rs.shape != null) {
	    rs.shape.drawShape(g2, rs.mode);
	  }
	}
      }
    }
  }

  public void addRepaintShape(DrawShape shape) {
    addRepaintShape(shape, DrawShapeConstants.DRAW_SELECTED);
  }

  public void removeRepaintShape(DrawShape shape) {
    if (shape != null) {
      Iterator<RShape> iter = repaintShapes.iterator();
      while (iter.hasNext()) {
	RShape rs = iter.next();
	if (rs.shape == shape) {
	  iter.remove();
	  break;
	}
      }
    }
  }

  public void removeRepaintShapes(int mode) {
    Iterator<RShape> iter = repaintShapes.iterator();
    while (iter.hasNext()) {
      RShape rs = iter.next();
      if (rs.mode == mode) {
	iter.remove();
      }
    }
  }

  public void addRepaintShape(DrawShape shape, int mode) {
    if (shape != null) {
      repaintShapes.add(new RShape(shape, mode));
    }
  }

  public void clearRepaintShapes() {
    repaintShapes.clear();
  }

  public void repaintShape(DrawShape shape) {
    repaintShape(shape, false);
  }

  public void repaintShape(DrawShape shape, boolean immediate) {
    if (debug) {
      System.out.println("DrawCanvas.repaintShape(): shape=" + shape);
    }

    if (shape != null) {
      // Rectangle2D r2 = shape.getBounds();
      Rectangle2D r2 = shape.getRepaintRect();
      repaintRect(r2, immediate);
    }
  }

  public void repaintRect(Rectangle2D rect) {
    repaintRect(rect, false, true);
  }

  public void repaintRect(Rectangle2D rect, boolean immediate) {
    repaintRect(rect, immediate, true);
  }

  public void repaintRect(Rectangle2D rect, boolean immediate,
			  boolean adjustBounds) {
    if (rect != null) {
      float d = 10f; // 3.0f;
      Rectangle2D r2 = rect;
      if (adjustBounds) {
	r2 = new Rectangle2D.Float((float) rect.getX() - d, (float) rect.getY() - d, 
				   (float) rect.getWidth() + 2 * d + 1, 
				   (float) rect.getHeight() + 2 * d + 1);
      }
      Rectangle r = virtualToPhysical(r2);

      if (debug) {
	System.out.println("DrawCanvas.repaintRect(): " + r);
      }

      if (immediate) {
	paintImmediately(r.x, r.y, r.width, r.height);
      } else {
	repaint(r.x, r.y, r.width, r.height);
      }
    }
  }

  public void setTransform(Graphics2D g2) {
    if (g2 != null) {
      AffineTransform t0 = g2.getTransform();
      if (t0 == null) {
	g2.setTransform(transform);
      } else {
	t0.concatenate(transform);
	g2.setTransform(t0);
      }
    }
  }

  public void focusGained(FocusEvent e) {
    hasFocus = true;
    // repaint();
  }

  public void setDrawFocus(boolean b) {
    drawFocus = b;
  }

  public void focusLost(FocusEvent e) {
    hasFocus = false;
    // repaint();
  }

  // public boolean isFocusTraversable() {
  @Override
  public boolean isFocusable() {
    return true;
  }

  public boolean getHasFocus() {
    return hasFocus;
  }

  public void addListener(DrawCanvasEventListener listener) {
    addKeyListener(listener);
    addMouseListener(listener);
    addMouseMotionListener(listener);
    registerKeyboardAction(new ActionListener() {
      public void actionPerformed(ActionEvent e) {
      }
    }, KeyStroke.getKeyStroke(KeyEvent.VK_TAB, 0, false), WHEN_FOCUSED);
  }

  protected void resetTransform() {
    if (transform == null) {
      transform = new AffineTransform();
    } else {
      transform.setToIdentity();
    }
    transform.scale(scaleFactor, scaleFactor);
    transform.translate(-xOrigin, -yOrigin);
  }

  public void setRenderingHints(Graphics2D g2) {
    if (view != null) {
      if (view.isUseAntialias()) {
	g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
			    RenderingHints.VALUE_ANTIALIAS_ON);
	g2.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING,
			    RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
      } else {
	g2.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING,
			    RenderingHints.VALUE_TEXT_ANTIALIAS_OFF);
	g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
			    RenderingHints.VALUE_ANTIALIAS_OFF);
      }
      if (view.isFractionalMetrics()) {
	g2.setRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS,
			    RenderingHints.VALUE_FRACTIONALMETRICS_ON);
      } else {
	g2.setRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS,
			    RenderingHints.VALUE_FRACTIONALMETRICS_OFF);
      }
    }
  }

  public boolean isThumbNail() {
    return thumbnail;
  }

  public void setThumbNail(boolean thumbnail) {
    this.thumbnail = thumbnail;
  }

  protected DrawCanvasEventListener listener;

  protected float xOrigin, yOrigin; // viewport origin (upper-left corner)

  protected float scaleFactor = 1.0f;

  /** transformation from user space to device space */
  protected AffineTransform transform = new AffineTransform(); // identity

  protected View view;

  protected ScrollCanvas scrollcanvas;

  protected Dimension preferedCanvasSize; // device space

  protected Rectangle2D virtualCanvasRectangle;

  /** whether the canvas currently has the focus */
  protected boolean hasFocus = false;

  /** whether it is during an editing session */
  protected boolean editMode = false;

  /** whether to draw a red rect to indicate focus */
  protected boolean drawFocus = true;

  protected boolean thumbnail = false;
  protected boolean drawThumbViewport = true;

  protected DrawCanvas master = null;

  protected List<RShape> repaintShapes = new ArrayList<RShape>(16);

  static class RShape {

    RShape(DrawShape shape, int mode) {
      this.shape = shape;
      this.mode = mode;
    }

    DrawShape shape;

    int mode;
  }

  // protected RepaintCollector repaintCollector;

  /** whether the font map needs to be initialized */
  // protected static boolean initFontMap = true;
  /**
         * Event listener for the drawcanvas delegate the events to the current
         * tool
         */
  class DrawCanvasEventListener 
    implements MouseListener, MouseMotionListener, MouseWheelListener, KeyListener {

    DrawCanvas canvas;

    DrawCanvasEventListener(DrawCanvas canvas) {
      this.canvas = canvas; 
    }

    public void mouseClicked(MouseEvent e) {
      p = e.getPoint();

      if (debug) {
	System.out.println("mouseClicked " + p);
      }

      DrawTool tool = (view != null) ? view.getCurrentTool() : null;

      // if (getHasFocus())
      requestFocusInWindow();

      if (tool != null) {
	int modifiers = e.getModifiersEx();
	int clickcount = e.getClickCount();
	int button = e.getButton();

	if (debug) {
	  System.out.println("mouseClicked modifiers = " + modifiers);
	}

	tool.mouseClicked(view, p, modifiers, clickcount);

	// if ((modifiers & InputEvent.BUTTON3_DOWN_MASK) != 0) {
	if (button == MouseEvent.BUTTON3) {
	  if (clickcount == 1) {
	    tool.mouseRButtonSingleClicked(view, p, modifiers);

	    if (debug) {
	      System.out.println("RButton clicked");
	    }
	  } else if (clickcount == 2) {
	    tool.mouseRButtonDoubleClicked(view, p, modifiers);

	    if (debug) {
	      System.out.println("RButton double clicked");
	    }
	  }
	  // } else if ((modifiers & InputEvent.BUTTON2_DOWN_MASK) !=
	  // 0) {
	} else if (button == MouseEvent.BUTTON2) {
	  if (clickcount == 1) {
	    tool.mouseMButtonSingleClicked(view, p, modifiers);

	    if (debug) {
	      System.out.println("MButton clicked");
	    }
	  } else if (clickcount == 2) {
	    tool.mouseMButtonDoubleClicked(view, p, modifiers);

	    if (debug) {
	      System.out.println("MButton double clicked");
	    }
	  }
	  // } else {
	} else if (button == MouseEvent.BUTTON1) {
	  if (clickcount == 1) {
	    tool.mouseLButtonSingleClicked(view, p, modifiers);

	    if (debug) {
	      System.out.println("LButton clicked");
	    }
	  } else if (clickcount == 2) {
	    tool.mouseLButtonDoubleClicked(view, p, modifiers);

	    if (debug) {
	      System.out.println("LButton double clicked");
	    }
	  }
	}
      }
    }

    public void mousePressed(MouseEvent e) {
      mouseButtonDown = true;
      p = e.getPoint();

      if (debug) {
	System.out.println("mousePressed " + p);
      }

      DrawTool tool = (view != null) ? view.getCurrentTool() : null;

      // if (getHasFocus())
      requestFocusInWindow();

      if (tool != null) {
	int modifiers = e.getModifiersEx();
	int button = e.getButton();

	if (debug) {
	  System.out.println("mousePressed modifiers = " + modifiers);
	}

	tool.mousePressed(view, p, modifiers);
	// if ((modifiers & InputEvent.BUTTON3_DOWN_MASK) != 0) {

	boolean popupTrigger = (((modifiers & InputEvent.SHIFT_DOWN_MASK) == 0) && 
				((e.isPopupTrigger() || 
				  (modifiers & InputEvent.BUTTON3_DOWN_MASK) != 0)));
	if (popupTrigger) {
	  tool.popupTriggered(view, p, modifiers);

	  if (debug) {
	    System.out.println("Popup triggered");
	  }
	} else if (button == MouseEvent.BUTTON3) {
	  tool.mouseRButtonPressed(view, p, modifiers);

	  if (debug) {
	    System.out.println("RButton pressed");
	  }
	  // } else if ((modifiers & InputEvent.BUTTON2_DOWN_MASK) !=
	  // 0) {
	} else if (button == MouseEvent.BUTTON2) {
	  tool.mouseMButtonPressed(view, p, modifiers);

	  if (debug) {
	    System.out.println("MButton pressed");
	  }
	  // } else {
	} else if (button == MouseEvent.BUTTON1) {
	  tool.mouseLButtonPressed(view, p, modifiers);

	  if (debug) {
	    System.out.println("LButton pressed");
	  }
	}
      }
    }

    public void mouseReleased(MouseEvent e) {
      mouseButtonDown = false;
      p = e.getPoint();

      if (debug) {
	System.out.println("mouseReleased " + p);
      }

      DrawTool tool = (view != null) ? view.getCurrentTool() : null;

      if (tool != null) {
	int modifiers = e.getModifiersEx();
	int button = e.getButton();

	if (debug) {
	  System.out.println("mouseReleased modifiers = " + modifiers);
	}

	tool.mouseReleased(view, p, modifiers);
	// if ((modifiers & InputEvent.BUTTON3_DOWN_MASK) != 0) {
	if (button == MouseEvent.BUTTON3) {
	  tool.mouseRButtonReleased(view, p, modifiers);

	  if (debug) {
	    System.out.println("RButton released");
	  }
	  // } else if ((modifiers & InputEvent.BUTTON2_DOWN_MASK) !=
	  // 0) {
	} else if (button == MouseEvent.BUTTON2) {
	  tool.mouseMButtonReleased(view, p, modifiers);

	  if (debug) {
	    System.out.println("MButton released");
	  }
	  // } else {
	} else if (button == MouseEvent.BUTTON1) {
	  tool.mouseLButtonReleased(view, p, modifiers);

	  if (debug) {
	    System.out.println("LButton released");
	  }
	}
      }
    }

    public void mouseEntered(MouseEvent e) {
      //if (e.getComponent() != canvas) return;

      p = e.getPoint();
      /*
      Dimension dim = canvas.getSize();
      if (p.x > 5 && 
	  p.x < dim.width - 5 &&
	  p.y > 5 && 
	  p.y < dim.height - 5) {
	return; // still inside canvas 
      }
      */

      int modifiers = e.getModifiersEx();

      if (debug) {
	System.out.println("mouseEntered " + p);
      }

      DrawTool tool = (view != null) ? view.getCurrentTool() : null;
      if (tool != null) {
	tool.mouseEntered(view, p, modifiers);
      }
    }

    public void mouseExited(MouseEvent e) {
      //if (e.getComponent() != canvas) return;

      p = e.getPoint();
      /*
      Dimension dim = canvas.getSize();
      if (p.x > 5 && 
	  p.x < dim.width - 5 &&
	  p.y > 5 && 
	  p.y < dim.height - 5) {
	return; // still inside canvas 
      }
      */

      int modifiers = e.getModifiersEx();

      if (debug) {
	System.out.println("mouseExited " + p);
      }

      DrawTool tool = (view != null) ? view.getCurrentTool() : null;
      if (tool != null) {
	tool.mouseExited(view, p, modifiers);
      }

      p = null;
    }

    public void mouseDragged(MouseEvent e) {
      p = e.getPoint();
      int modifiers = e.getModifiersEx();

      if (debug) {
	System.out.println("mouseDraged " + p);
      }

      DrawTool tool = (view != null) ? view.getCurrentTool() : null;
      if (tool != null) {
	tool.mouseDragged(view, p, modifiers);
      }
    }

    public void mouseMoved(MouseEvent e) {
      p = e.getPoint();
      int modifiers = e.getModifiersEx();

      if (debug) {
	System.out.println("mouseMoved " + p);
      }

      DrawTool tool = (view != null) ? view.getCurrentTool() : null;
      if (tool != null) {
	if (mouseButtonDown) {
	  tool.mouseDragged(view, p, modifiers);
	} else {
	  tool.mouseMoved(view, p, modifiers);
	}
      }
    }

    public void mouseWheelMoved(MouseWheelEvent e) {
      p = e.getPoint();
      int modifiers = e.getModifiersEx();
      int wheelRotation = e.getWheelRotation();

      if (debug) {
	System.out.println("mouseWheelMoved " + wheelRotation);
      }

      DrawTool tool = (view != null) ? view.getCurrentTool() : null;
      if (tool != null) {
	tool.mouseWheelMoved(view, p, wheelRotation, modifiers);
      }
    }

    public void keyTyped(KeyEvent e) {
      if (debug) {
	System.out.println("keyTyped");
      }

      DrawTool tool = (view != null) ? view.getCurrentTool() : null;
      if (tool != null) {
	tool.keyTyped(view, e.getKeyChar(), e.getKeyCode(), e.getModifiersEx());
      }
    }

    public void keyPressed(KeyEvent e) {
      if (debug) {
	System.out.println("keyPressed");
      }

      DrawTool tool = (view != null) ? view.getCurrentTool() : null;
      if (tool != null) {
	tool.keyPressed(view, e.getKeyChar(), e.getKeyCode(), e.getModifiersEx());
      }
    }

    public void keyReleased(KeyEvent e) {
      if (debug) {
	System.out.println("keyReleased");
      }

      DrawTool tool = (view != null) ? view.getCurrentTool() : null;
      if (tool != null) {
	tool.keyReleased(view, e.getKeyChar(), e.getKeyCode(), e.getModifiersEx());
      }
    }

    public Point getMouseLocation() {
      return p;
    }

    protected Point p;
  }

  protected boolean mouseButtonDown = false;

  protected static boolean debug = false;

}
